using System;
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using MFWebSockets.Core.Contract.Delegates;
using MFWebSockets.Core.Contract.Enums;
using MFWebSockets.Core.Contract.EventArgs;
using MFWebSockets.Core.Contract.Interfaces;
using MFWebSockets.Core.Utilities;
using MFWebSockets.Core.Utilities.Algorithms;
using MFWebSockets.Core.Utilities.Collections;
using MFWebSockets.Core.Utilities.Collections.Cookie;
using MFWebSockets.Core.Utilities.Exceptions;
using Microsoft.SPOT;

namespace MFWebSockets.Core.Clients
{
    public class WebSocketClient : IWebSocketClient, IDisposable
    {
        private readonly ILoggingClient loggingClient;

        private const int MaxTcpLength = 2048;
        private const int SocketBufferSize = 256;
        private readonly byte[] socketBuffer = new byte[SocketBufferSize];
        private readonly byte[] newLineMark = { 0x0d, 0x0a, 0x0d, 0x0a };
        private byte[] handShakeBytes;

        private readonly string host;
        private readonly string path;

        private readonly Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
                                    {
                                        SendTimeout = 5000,
                                        ReceiveTimeout = 5000
                                    };

        private int bytesSent;
        private int bytesReceived;
        private bool inSending;
        private ArrayList m_MessBuilder = new ArrayList();
        private const byte m_DataFrameStart = 0x00;
        private const byte m_DataFrameEnd = 0xFF;
        ConcurrentQueue m_MessagesBeingSent = new ConcurrentQueue();


        public WebSocketClient(Uri endpointUri, ILoggingClient loggingClient)
        {
            this.loggingClient = loggingClient;
            ConnectionStatus = ConnectionStatus.Disconnected;

            if (!(endpointUri.Scheme == "ws" || endpointUri.Scheme == "wss"))
                throw new ArgumentException("Only ws or wss are supported uri schemes.");

            var pos = endpointUri.AbsoluteUri.IndexOf('/', 5);

            if (pos <= 0)
                throw new ArgumentException("Invalid websocket address!");
            
            path = endpointUri.AbsoluteUri.Substring(pos);

            var hostInfo = endpointUri.AbsoluteUri.Substring(5, pos - 5).Split(':');

            if (hostInfo.Length != 2)
                throw new ArgumentException("Invalid websocket address! Ensure the port number is provided :80 for example.");

            int port;

            try { port = int.Parse(hostInfo[1]); }
            catch { throw new ArgumentException("Invalid websocket address!"); }

            host = hostInfo[0];

            try
            {
                var ipAddress = IPAddress.Parse(host);
                Endpoint = new IPEndPoint(ipAddress, port);
            }
            catch (ArgumentException)
            {
                try { Endpoint = new IPEndPoint(Dns.GetHostEntry(host).AddressList[0], port); }
                catch { throw new ArgumentException("Could not resolve host via DNS."); }
            }
            catch (ThreadAbortException) { return; }
         
            Close += OnClose;
            Open += OnOpen;
        }

        

        private void OnOpen(IWebSocketClient sender)
        {
            ConnectionStatus = ConnectionStatus.Connected;
        }

        private void OnClose(IWebSocketClient sender)
        {
            ConnectionStatus = ConnectionStatus.Disconnected;
        }

        public event OpenHandler Open;
        public event CloseHandler Close;
        public event MessageHandler Message;
        public ConnectionStatus ConnectionStatus { get; set; }

        public EndPoint Endpoint;



        public void Send(string message)
        {
            Thread.Sleep(1000);
        }

        public void Connect()
        {
            if (ConnectionStatus == ConnectionStatus.Connecting || 
                ConnectionStatus == ConnectionStatus.HandShaking ||
                ConnectionStatus == ConnectionStatus.Connected)
                return;

            ConnectionStatus = ConnectionStatus.Connecting;

            new Thread(() =>
                           {
                               try
                               {
                                   try
                                   {

                                       //IPEndPoint sender = new IPEndPoint(IPAddress.Any, 57220);
                                       //socket.Bind(sender);

                                       socket.Connect(Endpoint);
                                       var sent = BuildHandShake(out handShakeBytes);
                                       bytesSent += socket.SendTo(Encoding.UTF8.GetBytes(sent), Endpoint);
                                       
                                       loggingClient.Debug("Sent bytes: " + sent);

                                       bytesReceived = socket.ReceiveFrom(socketBuffer, 0, SocketBufferSize,
                                                                          SocketFlags.None, ref Endpoint);

                                       loggingClient.Debug("Recieved bytes: " +
                                                           Tools.GetString(socketBuffer, SocketBufferSize, bytesReceived));


                                   }
                                   catch (SocketException socketException)
                                   {
                                       SocketException(socketException);
                                   }
                                   catch
                                   {
                                       Dispose();
                                       throw;
                                   }

                                   ProcessHandshake();

                                   Open(this);

                                   while (ConnectionStatus == ConnectionStatus.Connected) StartReceive();
                               }
                               catch (ThreadAbortException)
                               {
                                   return;
                               }
                           }).Start();
        }

        private String BuildHandShake(out byte[] expectedResponse)
        {
            var secKey1 = new String(Encoding.UTF8.GetChars(GenerateSecKey()));
            var secKey2 = new String(Encoding.UTF8.GetChars(GenerateSecKey()));
            var secKey3 = GenerateSecKey(8);

            expectedResponse = GetResponseSecurityKey(secKey1, secKey2, secKey3);

            var handshakeBuilder = new StringBuilder();
            handshakeBuilder.AppendLine(string.Concat("GET ", path, " HTTP/1.1"));
            handshakeBuilder.AppendLine("Upgrade: WebSocket");
            handshakeBuilder.AppendLine("Connection: Upgrade");
            handshakeBuilder.AppendLine(string.Concat("Sec-WebSocket-Key2: ", secKey2));
            handshakeBuilder.AppendLine(string.Concat("Host: ", host));
            handshakeBuilder.AppendLine(string.Concat("Sec-WebSocket-Key1: ", secKey1));
            handshakeBuilder.AppendLine(string.Concat("Origin: ", host));
            handshakeBuilder.AppendLine();
            handshakeBuilder.Append(Encoding.UTF8.GetChars(secKey3));

            return handshakeBuilder.ToString();
        }

        public void Disconnect()
        {
            ConnectionStatus = ConnectionStatus.Disconnecting;

            
            socket.Close();


            // TODO: Complete this methods

            Close(this);
        }

        void ProcessSend(ref MessageArgs messageArgs)
        {
            if (null == messageArgs || null == messageArgs.DataFrame) return;
            try
            {
                bytesSent += socket.SendTo(messageArgs.DataFrame, 0, messageArgs.DataFrame.Length, SocketFlags.None, Endpoint);
                loggingClient.Debug("Sent bytes: " + messageArgs.Message);
            }
            catch (SocketException socketException)
            {
                SocketException(socketException);
            }
            catch (ThreadAbortException) { return; }
        }

        private void StartSendFromQueue()
        {
            //A boxed string
            object oMsg = null;
            //If we can't get a message return
            if (!(inSending = m_MessagesBeingSent.TryDequeue(out oMsg))) return;
            else do
                {
                    //Get the dequeued message out of the object box
                    var message = oMsg as MessageArgs;
                    //Send it
                    try
                    {
                        ProcessSend(ref message);
                    }
                    catch
                    {
                        //Whatever happened all it means to us (Worker) is that we should stop sending
                        break;
                    }
                    finally
                    {
                        oMsg = null;
                    }
                    //While there is still a object in the message to unbox                        
                } while (inSending = m_MessagesBeingSent.TryDequeue(out oMsg));
        }

        private void StartReceive()
        {
            try
            {
                var recieved = socket.ReceiveFrom(socketBuffer, 0, SocketBufferSize, SocketFlags.None, ref Endpoint);
                bytesReceived += recieved;

                if (recieved > 0 && (ConnectionStatus == ConnectionStatus.Connected || ConnectionStatus == ConnectionStatus.HandShaking)) 
                    ProcessReceive(socketBuffer, SocketBufferSize, recieved);

                if (!inSending && (ConnectionStatus == ConnectionStatus.Connected || ConnectionStatus == ConnectionStatus.HandShaking)) 
                    StartSendFromQueue();
            }
            catch (SocketException socketException)
            {
                SocketException(socketException);
            }
        }

        void ProcessReceive(byte[] buffer, int offset, int length)
        {
            if (m_MessBuilder.Count <= 0)
            {
                int startPos = Array.IndexOf(buffer, m_DataFrameStart, 0);
                if (startPos < 0) return;
                var count = offset + length - startPos;
                int endPos = Array.IndexOf(buffer, m_DataFrameEnd, startPos, count);
                if (endPos < 0)
                {
                    foreach (byte b in Microsoft.SPOT.Hardware.Utility.ExtractRangeFromArray(buffer, offset, length)) 
                        m_MessBuilder.Add(b);

                    if (ConnectionStatus == ConnectionStatus.Connected) 
                        Message(this, new MessageArgs
                                          {
                                              Message = new String(Encoding.UTF8.GetChars(buffer), startPos + 1, endPos - startPos - 1)
                                          });
                }
                if (endPos >= (offset + length - 1)) 
                    return;
                ProcessReceive(buffer, endPos + 1, offset + length - endPos - 1);
            }
            else
            {
                int endPos = Array.IndexOf(buffer, m_DataFrameEnd, offset, length);
                if (endPos < 0)
                {
                    foreach (byte b in Microsoft.SPOT.Hardware.Utility.ExtractRangeFromArray(buffer, offset, length))
                        m_MessBuilder.Add(b);
                    return;
                }

                int len = endPos - offset;
                Message(this, new MessageArgs
                                  {
                                      Message = new String(
                                          Encoding.UTF8.GetChars(
                                              Microsoft.SPOT.Hardware.Utility.ExtractRangeFromArray(buffer, offset, len)),
                                          offset, len)
                                  });

                if (endPos >= (offset + length - 1)) 
                    return;

                ProcessReceive(buffer, endPos + 1, offset + length - endPos - 1);
            }
        }
   
        protected virtual void ProcessHandshake()
        {
            //The first response usually looks something like this and is in response to the Handshake
            /*
             * HTTP/1.1 101 WebSocket Protocol Handshake\r\n
             * Upgrade: WebSocket\r\nConnection: Upgrade\r\n
             * Sec-WebSocket-Location: {m_Uri.AbsoluteUri}\r\n
             * Sec-WebSocket-Origin: {Host}\r\n\r\n
             * {HandShakeBytes}{DataFrame}
                 
             * * Where Data Frame = {0x00}{DATA}{0x255}
                 
             * * For Pusher {DATA} = something like below
             * An event - The name of the event,             some data - JSON Encoded
             * {\"event\":\"pusher:connection_established\",\"data\":\"{\\\"socket_id\\\":\\\"1188.541563\\\"}\"}
             * Only the {DATA} portion is given to the event. The expected response is in the m_HandShakeResponse field
            */

            if (bytesReceived > 0 || socket.Available > 0)
            {
                ConnectionStatus = ConnectionStatus.HandShaking;

            MatchHandShakeBytes:
                // TODO: Fix handshake verification bug.
                //var start = Tools.IndexOfBytes(newLineMark, SocketBufferSize - 1, newLineMark.Length, handShakeBytes, -2);
                //if (!Tools.Matches(socketBuffer, start, handShakeBytes.Length, handShakeBytes) && socket.Available == 0) 
                //    throw new WebSocketClientException("Handshake doesn't match");
                while (socket.Available > 0 && bytesReceived < MaxTcpLength)
                {
                    StartReceive(); 
                    goto MatchHandShakeBytes;
                }
                ProcessReceive(socketBuffer, SocketBufferSize, SocketBufferSize);
            }
            else
            {
                handShakeBytes = Tools.emptyBytes;
                loggingClient.Debug("No handshake recieved");
            }
        }

        internal static byte[] GetResponseSecurityKey(string secKey1, string secKey2, byte[] secKey3)
        {
            var replacer = new Regex("[^0-9]");
            
            var k1 = replacer.Replace(secKey1, "");
            var k2 = replacer.Replace(secKey2, "");
            
            var intK1 = Int64.Parse(k1);
            var intK2 = Int64.Parse(k2);
            
            var k1Spaces = Tools.CountChars(ref secKey1, ref Tools.Space);
            var k2Spaces = Tools.CountChars(ref secKey2, ref Tools.Space);
            var k1FinalNum = (int)(intK1 / k1Spaces);
            var k2FinalNum = (int)(intK2 / k2Spaces);
            
            var b1 = new byte[4];
            Microsoft.SPOT.Hardware.Utility.InsertValueIntoArray(b1, 0, 4, (uint)k1FinalNum);
            Tools.Reverse(b1);

            var b2 = new byte[4];
            Microsoft.SPOT.Hardware.Utility.InsertValueIntoArray(b2, 0, 4, (uint)k2FinalNum);
            Tools.Reverse(b2);

            var b3 = Encoding.UTF8.GetBytes(new String(Encoding.UTF8.GetChars(secKey3)));

            return MD5.GetHash(Tools.CombineArrays(b1, b2, b3));
        }

        internal static byte[] GenerateSecKey()
        {
            return GenerateSecKey(Tools.Random(10, 20));
        }

        internal static byte[] GenerateSecKey(int totalLen)
        {
            int tM1 = totalLen - 1;
            int spaceLen = Tools.Random(1, totalLen / 2 + 1);
            int charLen = Tools.Random(3, tM1 - spaceLen);
            int digLen = totalLen - spaceLen - charLen;
            var source = new ArrayList();
            for (int i = 0; i < spaceLen; ++i) source.Add(Tools.Space);
            for (int i = 0; i < charLen; ++i) source.Add((char)Tools.Random((int)'A', (int)'z'));
            for (int i = 0; i < digLen; ++i) source.Add((char)Tools.Random((int)'0', (int)'9'));
            var mixedChars = new byte[totalLen];
            for (int i = 0; i < tM1; ++i)
            {
                int pos = Tools.Random(source.Count - 1);
                object toAddRemove = source[pos];
                byte b = Tools.__BYTE_UNBOX__(toAddRemove);
                mixedChars[i] = b;
                source.Remove(toAddRemove);
            }
            mixedChars[tM1] = Tools.__BYTE_UNBOX__(source[0]);
            return mixedChars;
        }

        protected void SocketException(SocketException sEx)
        {
            switch (sEx.ErrorCode)
            {
                case 10050://Did not send the data within the defined timeout
                case 10060://Did not recieve a response within the defined timeout
                    return;
                default:
                    throw sEx;
            }
        }

        public void Dispose()
        {
            throw new NotImplementedException();
        }
    }
}
